package org.nativejpa.mapping;

import org.nativejpa.util.ClassUtils;
import org.nativejpa.util.AnnotationUtils;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;

import javax.persistence.PersistenceException;
import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.Version;
import javax.persistence.Table;
import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.sql.Timestamp;

/**
 * @author <a href="mailto:jfox.young@gmail.com">Young Yang</a>
 * @create 2009-3-12 16:08:00
 */
public class EntityMetadata {

    private static Log log = LogFactory.getLog(EntityMetadata.class);

    private Class<?> entityClass;

    private String tableName;

    // column name => column meta
    private Map<String, ColumnMetadata> columnMetadatas = new HashMap<String, ColumnMetadata>();

    // filed name => column meta
    private Map<String, ColumnMetadata> field2ColumnMetadatas = new HashMap<String, ColumnMetadata>();

    private ColumnMetadata pkColumnMetadata = null;
    private ColumnMetadata versionColumnMetadata = null;

    public EntityMetadata(Class entityClass) {
        this.entityClass = entityClass;
        parse();
    }

    private void parse(){
        if (ClassUtils.isPrimitiveClass(entityClass)) {
            return;
        }
        else if (MappingEntity.class.isAssignableFrom(entityClass)) {
            return;
        }
        else if (String.class.equals(entityClass) || ClassUtils.isPrimitiveClass(entityClass) || ClassUtils.isPrimitiveWrapperClass(entityClass)) {
            return;
        }

        // parse table name
        tableName = entityClass.getSimpleName();
        if (entityClass.isAnnotationPresent(Table.class)) {
            Table table = entityClass.getAnnotation(Table.class);
            if(table.name().trim().length() > 0) {
                tableName = table.name();
            }
        }

        if (entityClass.isInterface()) {
            throw new PersistenceException("Not supported result class: " + entityClass.getName() + ", only support primitive class, MappedEntity, and @Entity Class.");
        }
//        Map<String, ColumnMetadata> columnMetadatas = new HashMap<String, ColumnMetadata>();
        Field[] columnFields = AnnotationUtils.getAnnotatedFields(entityClass, Column.class);
        for (Field columnField : columnFields) {
            columnField.setAccessible(true);
            Column column = columnField.getAnnotation(Column.class);
            ColumnMetadata columnMetadata = new ColumnMetadata();
            String columnName = column.name();
            if (columnName.equals("")) {
                columnName = columnField.getName().toUpperCase();
            }
            columnMetadata.setName(columnName);
            columnMetadata.setField(columnField);
            columnMetadata.setDefineClass(entityClass);
            // is PK Column
            if(columnField.isAnnotationPresent(Id.class)) {
                columnMetadata.setPK(true);
                if(pkColumnMetadata == null) {
                    pkColumnMetadata = columnMetadata;
                }
                else {
                    log.warn("Only support one column as PK, use " + pkColumnMetadata.getName() + ", the second one is: " + columnMetadata.getName());
                }
            }
            if(columnField.isAnnotationPresent(Version.class)) {
                columnMetadata.setVersion(true);
                if(versionColumnMetadata == null) {
                    if(!versionColumnMetadata.getFieldType().equals(Short.class)
                    && !versionColumnMetadata.getFieldType().equals(short.class)
                    && !versionColumnMetadata.getFieldType().equals(Integer.class)
                    && !versionColumnMetadata.getFieldType().equals(int.class)
                    && !versionColumnMetadata.getFieldType().equals(Long.class)
                    && !versionColumnMetadata.getFieldType().equals(long.class)
                    && !versionColumnMetadata.getFieldType().equals(Timestamp.class )) {
                        throw new AnnotationParserException("@Version column " + versionColumnMetadata.getFieldName() + "'s type shoule be one of short, integer, long, timestamp.");
                    }
                    versionColumnMetadata = columnMetadata;
                }
                else {
                    throw new AnnotationParserException("Only one column can annoated as Version column, the first one is: " + versionColumnMetadata.getName() + ", the second one is: " + columnMetadata.getName());
                }
            }
            // store use key column Name and field name, so can be fetch by each of them
            columnMetadatas.put(column.name().toUpperCase(), columnMetadata);
            field2ColumnMetadatas.put(columnField.getName().toUpperCase(), columnMetadata);
        }

        Field[] joinColumnFields = AnnotationUtils.getAnnotatedFields(entityClass, JoinColumn.class);
        for (Field joinColumnField : joinColumnFields) {
            JoinColumn joinColumn = joinColumnField.getAnnotation(JoinColumn.class);
            JoinColumnMetadata joinColumnMetadata = new JoinColumnMetadata();
            if(joinColumn.name() == null || joinColumn.name().trim().equals("")) {
                throw new PersistenceException("Must specify name for JoinColumn: " + joinColumn);
            }
            joinColumnMetadata.setName(joinColumn.name());
            joinColumnMetadata.setField(joinColumnField);
            joinColumnMetadata.setDefineClass(entityClass);
            //TODO: should be specified
            joinColumnMetadata.setColumnDefinition(joinColumn.columnDefinition());
            //TODO: should be specified
            joinColumnMetadata.setReferencedColumnName(joinColumn.referencedColumnName());

            if(joinColumnField.getType().isArray()) {
                joinColumnMetadata.setTargetEntity(joinColumnField.getType().getComponentType());
            }
            else if(Collection.class.isAssignableFrom(joinColumnField.getType())) {
                if(joinColumnField.getGenericType() instanceof ParameterizedType){ // Generic Type collection
                    joinColumnMetadata.setTargetEntity((Class<?>)((ParameterizedType)joinColumnField.getGenericType()).getActualTypeArguments()[0]);
                }
                else {
                    // 没有指定泛型
                    joinColumnMetadata.setTargetEntity(MappingEntity.class);
                }
            }
            else {
                joinColumnMetadata.setTargetEntity(joinColumnMetadata.getFieldType());
            }

            columnMetadatas.put(joinColumnMetadata.getName().toUpperCase(), joinColumnMetadata);
            field2ColumnMetadatas.put(joinColumnField.getName().toUpperCase(), joinColumnMetadata);
        }
    }

    public Collection<ColumnMetadata> getColumnMetadatas(){
        return columnMetadatas.values();
    }

    public ColumnMetadata getColumnMetadata(String columnName) {
        return isColumnExisted(columnName) ? columnMetadatas.get(columnName.toUpperCase()) : field2ColumnMetadatas.get(columnName.toUpperCase());
    }

    public boolean isColumnExisted(String columnName) {
        return columnMetadatas.containsKey(columnName.toUpperCase());
    }

    public boolean isFieldExisted(String fieldName) {
        return field2ColumnMetadatas.containsKey(fieldName.toUpperCase());
    }

    public Collection<ColumnMetadata> getNormalColumnMetadatas() {
        List<ColumnMetadata> columnMetadatas = new ArrayList<ColumnMetadata>();
        Collection<ColumnMetadata> allColumnMetadatas = getColumnMetadatas();
        for (ColumnMetadata columnMetadata : allColumnMetadatas) {
            if (!(columnMetadata instanceof JoinColumnMetadata)) {
                columnMetadatas.add(columnMetadata);
            }
        }
        return columnMetadatas;
    }


    public Collection<JoinColumnMetadata> getJoinColumnMetadatas() {
        List<JoinColumnMetadata> joinColumnMetadatas = new ArrayList<JoinColumnMetadata>();
        Collection<ColumnMetadata> allColumnMetadatas = getColumnMetadatas();
        for (ColumnMetadata columnMetadata : allColumnMetadatas) {
            if (columnMetadata instanceof JoinColumnMetadata) {
                joinColumnMetadatas.add((JoinColumnMetadata) columnMetadata);
            }
        }
        return joinColumnMetadatas;
    }

    public ColumnMetadata getPKColumnMetadata(){
        return pkColumnMetadata;
    }

    public ColumnMetadata getVersionColumnMetadata() {
        return versionColumnMetadata;
    }

    public String getTableName() {
        return tableName;
    }
}
